// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.credentialStore.keePass

import com.intellij.credentialStore.*
import com.intellij.credentialStore.kdbx.IncorrectMasterPasswordException
import com.intellij.credentialStore.kdbx.KdbxPassword
import com.intellij.credentialStore.kdbx.KeePassDatabase
import com.intellij.credentialStore.kdbx.loadKdbx
import com.intellij.ide.passwordSafe.PasswordStorage
import com.intellij.openapi.application.PathManager
import com.intellij.util.io.delete
import com.intellij.util.io.exists
import com.intellij.util.io.safeOutputStream
import org.jetbrains.annotations.TestOnly
import java.nio.file.Path
import java.security.SecureRandom
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean

internal const val DB_FILE_NAME = "c.kdbx"

internal fun getDefaultKeePassBaseDirectory() = PathManager.getConfigDir()

internal fun getDefaultMasterPasswordFile() = getDefaultKeePassBaseDirectory().resolve(MASTER_KEY_FILE_NAME)

/**
 * preloadedMasterKey [MasterKey.value] will be cleared
 */
internal class KeePassCredentialStore constructor(internal val dbFile: Path, private val masterKeyStorage: MasterKeyFileStorage, preloadedDb: KeePassDatabase? = null) : BaseKeePassCredentialStore() {
  constructor(dbFile: Path, masterKeyFile: Path) : this(dbFile, MasterKeyFileStorage(masterKeyFile), null)

  private val isNeedToSave: AtomicBoolean

  override var db: KeePassDatabase = if (preloadedDb == null) {
    isNeedToSave = AtomicBoolean(false)
    when {
      dbFile.exists() -> {
        val masterPassword = masterKeyStorage.load() ?: throw IncorrectMasterPasswordException(isFileMissed = true)
        loadKdbx(dbFile, KdbxPassword.createAndClear(masterPassword))
      }
      else -> KeePassDatabase()
    }
  }
  else {
    isNeedToSave = AtomicBoolean(true)
    preloadedDb
  }

  val masterKeyFile: Path
    get() = masterKeyStorage.passwordFile

  @Synchronized
  @TestOnly
  fun reload() {
    val key = masterKeyStorage.load()!!
    val kdbxPassword = KdbxPassword(key)
    key.fill(0)
    db = loadKdbx(dbFile, kdbxPassword)
    isNeedToSave.set(false)
  }

  @Synchronized
  fun save(masterKeyEncryptionSpec: EncryptionSpec) {
    if (!isNeedToSave.compareAndSet(true, false) && !db.isDirty) {
      return
    }

    try {
      val secureRandom = createSecureRandom()
      val masterKey = masterKeyStorage.load()
      val kdbxPassword: KdbxPassword
      if (masterKey == null) {
        val key = generateRandomMasterKey(masterKeyEncryptionSpec, secureRandom)
        kdbxPassword = KdbxPassword(key.value!!)
        masterKeyStorage.save(key)
      }
      else {
        kdbxPassword = KdbxPassword(masterKey)
        masterKey.fill(0)
      }

      dbFile.safeOutputStream().use {
        db.save(kdbxPassword, it, secureRandom)
      }
    }
    catch (e: Throwable) {
      // schedule save again
      isNeedToSave.set(true)
      LOG.error("Cannot save password database", e)
    }
  }

  @Synchronized
  fun isNeedToSave() = isNeedToSave.get() || db.isDirty

  @Synchronized
  fun deleteFileStorage() {
    try {
      dbFile.delete()
    }
    finally {
      masterKeyStorage.save(null)
    }
  }

  fun clear() {
    db.rootGroup.removeGroup(ROOT_GROUP_NAME)
    isNeedToSave.set(db.isDirty)
  }

  @TestOnly
  fun setMasterPassword(masterKey: MasterKey, secureRandom: SecureRandom) {
    // KdbxPassword hashes value, so, it can be cleared before file write (to reduce time when master password exposed in memory)
    saveDatabase(dbFile, db, masterKey, masterKeyStorage, secureRandom)
  }

  override fun markDirty() {
    isNeedToSave.set(true)
  }
}

class InMemoryCredentialStore : BaseKeePassCredentialStore(), PasswordStorage {
  override val db = KeePassDatabase()

  override fun markDirty() {
  }
}

internal fun generateRandomMasterKey(masterKeyEncryptionSpec: EncryptionSpec, secureRandom: SecureRandom): MasterKey {
  val bytes = secureRandom.generateBytes(512)
  return MasterKey(Base64.getEncoder().withoutPadding().encode(bytes), isAutoGenerated = true, encryptionSpec = masterKeyEncryptionSpec)
}

internal fun saveDatabase(dbFile: Path, db: KeePassDatabase, masterKey: MasterKey, masterKeyStorage: MasterKeyFileStorage, secureRandom: SecureRandom) {
  val kdbxPassword = KdbxPassword(masterKey.value!!)
  masterKeyStorage.save(masterKey)
  dbFile.safeOutputStream().use {
    db.save(kdbxPassword, it, secureRandom)
  }
}

internal fun copyTo(from: Map<CredentialAttributes, Credentials>, store: CredentialStore) {
  for ((k, v) in from) {
    store.set(k, v)
  }
}